--- Transform Grid.
-- A grid that can be rotated, flipped, and translated.
-- @module ROT.Type.TransformGrid
local ROT = require 'rot'
local TransformGrid = ROT.Type.Grid:extend()

TransformGrid.DIR_UP = 1
TransformGrid.DIR_RIGHT = 2
TransformGrid.DIR_DOWN = 3
TransformGrid.DIR_LEFT = 4

function TransformGrid:init()
    self:clear()
    self._rotation = 2
    self._dirty = true
end

TransformGrid._setCell = TransformGrid.setCell
function TransformGrid:setCell(...)
    self._dirty = true
    return self:_setCell(...)
end

TransformGrid._removeCell = TransformGrid.removeCell
function TransformGrid:removeCell(...)
    self._dirty = true
    return self:_removeCell(...)
end

function TransformGrid:getBoundary()
    if not self._dirty then
        return self._x1, self._y1, self._x2, self._y2
    end
    self._dirty = false
    local x1, y1, x2, y2 = math.huge, math.huge, -math.huge, -math.huge
    for i = #self._points - 1, 1, -2 do
        local x, y = self._points:peek(i)
        x1, y1 = math.min(x1, x), math.min(y1, y)
        x2, y2 = math.max(x2, x), math.max(y2, y)
    end
    self._x1, self._y1, self._x2, self._y2 = x1, y1, x2, y2
    return x1, y1, x2, y2
end

--- Set flip transformation.
-- Sets whether the grid is flipped horizontally or vertically.
-- @tparam boolean x If true, the grid is flipped horizontally.
-- @tparam boolean y If true, the grid is flipped vertically.
-- @treturn TransformGrid self
function TransformGrid:flip(x, y)
    self._flipX = x
    self._flipY = y
    self:checkTransform()
    return self
end

--- Set rotation transformation.
-- Sets a new rotation direction for the grid.
-- @tparam int dir The directon to rotate the right side towards.
-- 1=up, 2=right, 3=down, 4=left.
-- @treturn TransformGrid self
function TransformGrid:rotate(dir)
    self._rotation = ((dir - 1) % 4) + 1
    self:checkTransform()
    return self
end

function TransformGrid:checkTransform()
    self._transformed = self._flipX or self._flipY
        or self._rotation == TransformGrid.DIR_RIGHT
    return self
end

function TransformGrid:_flipPoint(x, y)
    local x1, y1, x2, y2 = self:getBoundary()
    if self._flipX then x = x1 + x2 - x end
    if self._flipY then y = y1 + y2 - y end
    return x, y
end

function TransformGrid:_rotatePoint(dir, x, y)
    if dir == TransformGrid.DIR_RIGHT then return x, y end
    local x1, y1, x2, y2 = self:getBoundary()
    local cx, cy = (x1 + x2) * 0.5, (y1 + y2) * 0.5 -- get bounding box center
    local d = ROT.DIRS.FOUR[dir] -- new direction to face, "d"
    x, y = x - cx, y - cy -- translate center to 0, 0
    x, y = x*d[1] + y*-d[2], x*d[2] + y*d[1] -- rotate right side towards "d"
    x, y = x + cx, y + cy -- move center back to original position
    return math.floor(x), math.floor(y)
end

function TransformGrid:transform(x, y)
    -- if not self._transformed then return x, y end
    x, y = self:_flipPoint(x, y)
    x, y = self:_rotatePoint(self._rotation, x, y)
    return x, y
end

function TransformGrid:next(i)
    i = i - 2
    if i > 0 then
        local x, y = self:transform(self._points:peek(i))
        return i, x, y, self._values[i]
    end
end

function TransformGrid:invoke(callback)
    for i = #self._points - 1, 1, -2 do
        local x, y = self:transform(self._points:peek(i))
        callback(x, y, self._values[i])
    end
end

function TransformGrid:untransform(x, y)
    if not self._transformed then return x, y end
    local dir =
        self._rotation == TransformGrid.DIR_UP and TransformGrid.DIR_DOWN
        or self._rotation == TransformGrid.DIR_DOWN and TransformGrid.DIR_UP
        or self._rotation
    x, y = self:_rotatePoint(dir, x, y)
    x, y = self:_flipPoint(x, y)
    return x, y
end

return TransformGrid
